using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using Needle.Core.Injection;
using Needle.Framework;
using Needle.Utils;

namespace Needle.Core.Builder {

    /// <summary>
    /// Utilityclass to construct dynamic types.
    /// </summary>
    public class ClassBuilder : Component {

        private ModuleBuilder module;

        /// <summary>
        /// Initializes a new instance.
        /// </summary>
        [Inject]
        public ClassBuilder([Inject(Name = "classmodule")]ModuleBuilder module) {
            Ensure.NotNull(module, "module");
            this.module = module;
        }

        private TypeBuilder m_Type;
        /// <summary>
        /// The typebuilder
        /// </summary>
        public TypeBuilder Type {
            get { return m_Type; }
        }

        /// <summary>
        /// Creates a new typebuilder.
        /// </summary>
        public void DefineType(string typeName, TypeAttributes attrs, Type parent) {
            Ensure.IsNull(m_Type, "Type already created");
            Ensure.NotNullOrEmpty(typeName, "typeName");
            Ensure.NotNull(parent, "parent");

            m_Type = module.DefineType(typeName, attrs, parent);
        }
        /// <summary>
        /// Overrides a property defined in the overriden type. 
        /// Creates a backingfield and the appropriate accessors.
        /// </summary>
        /// <param name="propertyName">The name of the property to override.</param>
        /// <param name="overridden">The type that declares the property to override.</param>
        /// <returns>The backing field for the property.</returns>
        public FieldBuilder OverrideProperty(string propertyName, Type overridden)
        {
            Ensure.NotNull(m_Type, "Type not defined yet, call DefineType(...) first.");
            Ensure.NotNullOrEmpty(propertyName, "propertyName");
            Ensure.NotNull(overridden, "overridden");
            // Get Property sig
            PropertyInfo overriddenProp = overridden.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
            Ensure.NotNull(overriddenProp, "propertyName", overridden.FullName + " does not have '" + propertyName + "' defined as a property");

            Type propType = overriddenProp.PropertyType;
            if (overriddenProp.GetIndexParameters().Length > 0)
            {
                throw new NotSupportedException("Overriding properties with index parameters is not supported by this method.");
            }

            if (!overridden.IsInterface)
            { // we need to check if the accessors can be overridden.
                System.Reflection.MethodInfo[] accessors = overriddenProp.GetAccessors();
                foreach (System.Reflection.MethodInfo acc in accessors)
                {
                    if ((acc.Attributes & MethodAttributes.Final) == MethodAttributes.Final)
                    {
                        throw new InvalidOperationException("Cannot override property " + propertyName + " because accessor " + acc.Name + " is declared final");
                    }
                }
            }

            // backing field
            FieldBuilder backField = m_Type.DefineField("_" + overriddenProp.Name,
                                                        propType,
                                                        FieldAttributes.Private);
            // property
            PropertyBuilder prop = m_Type.DefineProperty(overriddenProp.Name,
                                                        PropertyAttributes.None,
                                                        propType,
                                                        null);

            ILGenerator gen;
            // getter
            if (overriddenProp.CanRead)
            {
                MethodBuilder getter = m_Type.DefineMethod("get_" + overriddenProp.Name,
                                                            MethodAttributes.Public
                                                            | MethodAttributes.Virtual
                                                            | MethodAttributes.SpecialName);
                getter.SetReturnType(propType);

                gen = getter.GetILGenerator();
                gen.Emit(OpCodes.Ldarg_0);
                gen.Emit(OpCodes.Ldfld, backField);
                gen.Emit(OpCodes.Ret);

                prop.SetGetMethod(getter);
                m_Type.DefineMethodOverride(prop.GetGetMethod(), overriddenProp.GetGetMethod());
            }

            // setter
            if (overriddenProp.CanWrite)
            {
                MethodBuilder setter = m_Type.DefineMethod("set_" + overriddenProp.Name,
                                                           MethodAttributes.Public
                                                           | MethodAttributes.Virtual
                                                           | MethodAttributes.SpecialName);
                setter.SetParameters(propType);
                ParameterBuilder value = setter.DefineParameter(1, ParameterAttributes.None, "value");

                gen = setter.GetILGenerator();
                gen.Emit(OpCodes.Ldarg_0);
                gen.Emit(OpCodes.Ldarg_1);
                gen.Emit(OpCodes.Stfld, backField);
                gen.Emit(OpCodes.Ret);

                prop.SetSetMethod(setter);
                m_Type.DefineMethodOverride(prop.GetSetMethod(), overriddenProp.GetSetMethod());
            }

            return backField;
        }

        /// <summary>
        /// Creates an override of a method defined in the overriden type. 
        /// </summary>
        /// <param name="methodName">The name of the method to override.</param>
        /// <param name="overridden">The type that declares the method to override.</param>
        /// <param name="attrs">the method attributes.</param>
        /// <returns>The MethodBuilder to generate the body of the method.</returns>
        public MethodBuilder OverrideMethod(string methodName, Type overridden, MethodAttributes attrs)
        {
            Ensure.NotNull(m_Type, "Type not defined yet, call DefineType(...) first.");
            Ensure.NotNullOrEmpty(methodName, "methodName");
            Ensure.NotNull(overridden, "overridden");

            // Get method descriptor
            System.Reflection.MethodInfo overriddenMethod = overridden.GetMethod(methodName,
                                                               BindingFlags.Instance
                                                               | BindingFlags.Public
                                                               | BindingFlags.NonPublic);

            Ensure.NotNull(overriddenMethod, "methodName", methodName + " not found");
            Ensure.IsTrue(overriddenMethod.IsVirtual, "methodName", methodName + " is not overridable");

            // Define method
            MethodBuilder method = Type.DefineMethod(overriddenMethod.Name, attrs);

            Type[] genericArgTypes = overriddenMethod.GetGenericArguments();
            if (genericArgTypes.Length > 0)
            {
                GenericTypeParameterBuilder[] gtb;
                gtb = method.DefineGenericParameters(
                            Array.ConvertAll<System.Type, String>(genericArgTypes,
                                delegate(Type t)
                                {
                                    return t.Name;
                                }));
            }

            method.SetReturnType(overriddenMethod.ReturnType);

            ParameterInfo[] pInfos = overriddenMethod.GetParameters();
            Type[] pTypes = Array.ConvertAll<ParameterInfo, Type>(pInfos,
                        delegate(ParameterInfo p)
                        {
                            return p.ParameterType;
                        });
            method.SetParameters(pTypes);

            List<ParameterBuilder> pb = new List<ParameterBuilder>();
            Array.ForEach<ParameterInfo>(pInfos, delegate(ParameterInfo p)
            {
                pb.Add(method.DefineParameter(p.Position, p.Attributes, p.Name));
            });
            Type.DefineMethodOverride(method, overriddenMethod);
            return method;
        }
    }
}
